Análise de Dados preliminar com Python & Jupyter Notebooks¶
Case - Cancelamento de Clientes¶
Uma empresa hipotética com mais de 800 mil clientes deseja investigar por que a maioria de seus clientes registrados na base total são inativos (ou seja, cancelaram o serviço).
Essa análise de dados tem intuito de identificar os principais motivos desses cancelamentos e quais as ações mais eficientes para reduzir esse número.
Base de dados: cancelamentos.csv
### Magic commands
# In Jupyter Notebooks Online use %pip install
# In Vscode or other Python environments use !pip install
# É possível fazer a instalação de pacotes diretamente nos notebooks, como se fosse um terminal. Basta clicar no botão e rodar.
# !pip install pandas ---> Biblioteca bastante usada para manipular base de dados.
# !pip install numpy ---> Fornece suporte para matrizes e arrays grandes e multidimensionais, juntamente com uma coleção de funções matemáticas para operar nessas matrizes.
# !pip install openpyxl ---> Uma biblioteca Python para leitura e gravação de arquivos Excel (XLSX).
# !pip install plotly ---> Possibilita a criação de gráficos para a visualização de dados interativos e dinâmicos. É uma biblioteca conhecida por sua facilidade de uso.
# HTML export (with graphs)
# import plotly.io as pio
# pio.renderers.default = 'notebook'
# PDF export (with graphs)
# !pip install Pyppeteer
# !pyppeteer-install
# %pip install --upgrade plotly
# %pip install -q --upgrade nbformat
# %pip install --upgrade notebook
# import plotly
# print(plotly.__version__) # OUTPUT: 5.17.0
# ETAPA 1: Importar e visualizar a base de dados
import pandas as pd
tabela = pd.read_csv("cancelamentos.csv")
# "Informação que não ajuda, atrapalha". É importante remover a coluna dos IDs dos usuários, visto que não ajuda na análise
tabela = tabela.drop(columns="CustomerID")
# Visualizar a base de dados
print("Visualização da base de dados (sem modificações): ")
display(tabela) # Função especial do Jupyte Notebooks
Visualização da base de dados (sem modificações):
| idade | sexo | tempo_como_cliente | frequencia_uso | ligacoes_callcenter | dias_atraso | assinatura | duracao_contrato | total_gasto | meses_ultima_interacao | cancelou | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 30.0 | Female | 39.0 | 14.0 | 5.0 | 18.0 | Standard | Annual | 932.00 | 17.0 | 1.0 |
| 1 | 65.0 | Female | 49.0 | 1.0 | 10.0 | 8.0 | Basic | Monthly | 557.00 | 6.0 | 1.0 |
| 2 | 55.0 | Female | 14.0 | 4.0 | 6.0 | 18.0 | Basic | Quarterly | 185.00 | 3.0 | 1.0 |
| 3 | 58.0 | Male | 38.0 | 21.0 | 7.0 | 7.0 | Standard | Monthly | 396.00 | 29.0 | 1.0 |
| 4 | 23.0 | Male | 32.0 | 20.0 | 5.0 | 8.0 | Basic | Monthly | 617.00 | 20.0 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 881661 | 42.0 | Male | 54.0 | 15.0 | 1.0 | 3.0 | Premium | Annual | 716.38 | 8.0 | 0.0 |
| 881662 | 25.0 | Female | 8.0 | 13.0 | 1.0 | 20.0 | Premium | Annual | 745.38 | 2.0 | 0.0 |
| 881663 | 26.0 | Male | 35.0 | 27.0 | 1.0 | 5.0 | Standard | Quarterly | 977.31 | 9.0 | 0.0 |
| 881664 | 28.0 | Male | 55.0 | 14.0 | 2.0 | 0.0 | Standard | Quarterly | 602.55 | 2.0 | 0.0 |
| 881665 | 31.0 | Male | 48.0 | 20.0 | 1.0 | 14.0 | Premium | Quarterly | 567.77 | 21.0 | 0.0 |
881666 rows × 11 columns
# ETAPA 2: Tratar problemas da base de dados
# Com o método .info() é possível encontrar valores vazios e valores do tipo errado na tabela
display(tabela.info())
# Joga fora todas as linhas que tem algum valor vazio
tabela = tabela.dropna()
# Agora a tabela foi adequadamente tratada e está pronta p/ ser analisada
display(tabela.info())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 881666 entries, 0 to 881665 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 idade 881664 non-null float64 1 sexo 881664 non-null object 2 tempo_como_cliente 881663 non-null float64 3 frequencia_uso 881663 non-null float64 4 ligacoes_callcenter 881664 non-null float64 5 dias_atraso 881664 non-null float64 6 assinatura 881661 non-null object 7 duracao_contrato 881663 non-null object 8 total_gasto 881664 non-null float64 9 meses_ultima_interacao 881664 non-null float64 10 cancelou 881664 non-null float64 dtypes: float64(8), object(3) memory usage: 74.0+ MB
None
<class 'pandas.core.frame.DataFrame'> Index: 881659 entries, 0 to 881665 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 idade 881659 non-null float64 1 sexo 881659 non-null object 2 tempo_como_cliente 881659 non-null float64 3 frequencia_uso 881659 non-null float64 4 ligacoes_callcenter 881659 non-null float64 5 dias_atraso 881659 non-null float64 6 assinatura 881659 non-null object 7 duracao_contrato 881659 non-null object 8 total_gasto 881659 non-null float64 9 meses_ultima_interacao 881659 non-null float64 10 cancelou 881659 non-null float64 dtypes: float64(8), object(3) memory usage: 80.7+ MB
None
# ETAPA 3: Primeira verificação -> Descobrir o percentual de clientes que cancelou
# Na coluna "cancelou" da tabela, value_counts() agrupa valores em categoria (int64)
display( tabela["cancelou"].value_counts() )
# Com o parâmetro 'normalize' verdadeiro, a função retorna em porcentagem (float64)
# Normalizar: indice_total / indice_de_valor_especifico
# Logo, 499993 / 881659 --> para quem cancelou & 381666 / 881659 para quem não cancelou
display( tabela["cancelou"].value_counts(normalize=True) )
# EXTRA: Visualizando em porcentuual ao invés de float (usando o método .map() do Python)
# .map("{:.1%}".format) --> Códigos de formatação são escritos como string "{:.1%}"
# "{:.1%}" --> Significa: Mostre o resultado como percentual e com apenas uma casa depois da vírgula
# OBS: É melhor não usar pois transforma o tipo de dado em objeto, o que não permite realizar cálculos
# CONCLUSÃO: Mais de 50% dos clientes cancelaram
cancelou 1.0 499993 0.0 381666 Name: count, dtype: int64
cancelou 1.0 0.567105 0.0 0.432895 Name: proportion, dtype: float64
Percentual de clientes que cancelaram suas assinaturas: 56%¶
# Que tipo de contrato (mensal, trimestral, anual) tende a ser mais cancelado?
display( tabela["duracao_contrato"].value_counts() )
display( tabela["duracao_contrato"].value_counts(normalize=True).map("{:.1%}".format) )
duracao_contrato Annual 354395 Quarterly 353059 Monthly 174205 Name: count, dtype: int64
duracao_contrato Annual 40.2% Quarterly 40.0% Monthly 19.8% Name: proportion, dtype: object
# Agrupamento (uma tabelinha já com os dados calculados de cada categoria que ajuda com a análise)
# Fazendo a média de cada coluna numérica
tabela_agrupada = tabela.groupby("duracao_contrato").mean(numeric_only=True)
display(tabela_agrupada)
| idade | tempo_como_cliente | frequencia_uso | ligacoes_callcenter | dias_atraso | total_gasto | meses_ultima_interacao | cancelou | |
|---|---|---|---|---|---|---|---|---|
| duracao_contrato | ||||||||
| Annual | 38.842165 | 31.446186 | 15.880213 | 3.263401 | 12.465156 | 651.697738 | 14.236107 | 0.460760 |
| Monthly | 41.552407 | 30.538555 | 15.499274 | 4.985649 | 15.007267 | 550.616435 | 15.478012 | 1.000000 |
| Quarterly | 38.830938 | 31.419916 | 15.886662 | 3.265245 | 12.460863 | 651.427783 | 14.234544 | 0.460255 |
O tipo de assinatura mais cancelada é a mensal (100% de cancelamento)¶
CONCLUSÃO: Todos os clientes do contrato mensal cancelaram.
CAUSA: Provavelmente porque o plano não oferece benefícios satisfatórios.
SUGESTÃO: Oferecer desconto nos contratos anuais/trimestrais (pois eles são melhores). Ou melhorar os benefícios do contrato mensal.
# Agora que já se sabe porque as pessoas do plano mensal cancelaram, é hora de excluí-las da tabela para analisar outras causas de cancelamento adjacentes
# Excluindo a coluna do contrato mensal e armazenando em uma nova variável (tabela_filtrada)
# A variável exclui_mensal conterá um valor booleano (True ou False) para cada linha da coluna "duracao_contrato" da tabela
exclui_mensal = tabela["duracao_contrato"] != "Monthly"
# "True" : Não são do plano mensal
# "False" : São do plano mensal
display(exclui_mensal)
0 True
1 False
2 True
3 False
4 False
...
881661 True
881662 True
881663 True
881664 True
881665 True
Name: duracao_contrato, Length: 881659, dtype: bool
# Comparando tabelas
# A tabela resultante só vai mostrar as linhas que retornarem "True" na operação realizada pela variável exclui_mensal
tabela_filtrada = tabela[exclui_mensal] # Com o pandas é possível passar uma condição dentro dos colchetes
#OBS: Aqui não seria possível usar o método .drop(), pois ele só funciona com colunas e "Monthly" é uma categoria presente em linhas
print("Tabela normal (com Monthly):")
display(tabela)
print("Tabela filtrada (sem Monthly):")
display(tabela_filtrada) # A tabela mostrada excluiu todas as linhas com contrato mensal
Tabela normal (com Monthly):
| idade | sexo | tempo_como_cliente | frequencia_uso | ligacoes_callcenter | dias_atraso | assinatura | duracao_contrato | total_gasto | meses_ultima_interacao | cancelou | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 30.0 | Female | 39.0 | 14.0 | 5.0 | 18.0 | Standard | Annual | 932.00 | 17.0 | 1.0 |
| 1 | 65.0 | Female | 49.0 | 1.0 | 10.0 | 8.0 | Basic | Monthly | 557.00 | 6.0 | 1.0 |
| 2 | 55.0 | Female | 14.0 | 4.0 | 6.0 | 18.0 | Basic | Quarterly | 185.00 | 3.0 | 1.0 |
| 3 | 58.0 | Male | 38.0 | 21.0 | 7.0 | 7.0 | Standard | Monthly | 396.00 | 29.0 | 1.0 |
| 4 | 23.0 | Male | 32.0 | 20.0 | 5.0 | 8.0 | Basic | Monthly | 617.00 | 20.0 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 881661 | 42.0 | Male | 54.0 | 15.0 | 1.0 | 3.0 | Premium | Annual | 716.38 | 8.0 | 0.0 |
| 881662 | 25.0 | Female | 8.0 | 13.0 | 1.0 | 20.0 | Premium | Annual | 745.38 | 2.0 | 0.0 |
| 881663 | 26.0 | Male | 35.0 | 27.0 | 1.0 | 5.0 | Standard | Quarterly | 977.31 | 9.0 | 0.0 |
| 881664 | 28.0 | Male | 55.0 | 14.0 | 2.0 | 0.0 | Standard | Quarterly | 602.55 | 2.0 | 0.0 |
| 881665 | 31.0 | Male | 48.0 | 20.0 | 1.0 | 14.0 | Premium | Quarterly | 567.77 | 21.0 | 0.0 |
881659 rows × 11 columns
Tabela filtrada (sem Monthly):
| idade | sexo | tempo_como_cliente | frequencia_uso | ligacoes_callcenter | dias_atraso | assinatura | duracao_contrato | total_gasto | meses_ultima_interacao | cancelou | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 30.0 | Female | 39.0 | 14.0 | 5.0 | 18.0 | Standard | Annual | 932.00 | 17.0 | 1.0 |
| 2 | 55.0 | Female | 14.0 | 4.0 | 6.0 | 18.0 | Basic | Quarterly | 185.00 | 3.0 | 1.0 |
| 5 | 51.0 | Male | 33.0 | 25.0 | 9.0 | 26.0 | Premium | Annual | 129.00 | 8.0 | 1.0 |
| 6 | 58.0 | Female | 49.0 | 12.0 | 3.0 | 16.0 | Standard | Quarterly | 821.00 | 24.0 | 1.0 |
| 7 | 55.0 | Female | 37.0 | 8.0 | 4.0 | 15.0 | Premium | Annual | 445.00 | 30.0 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 881661 | 42.0 | Male | 54.0 | 15.0 | 1.0 | 3.0 | Premium | Annual | 716.38 | 8.0 | 0.0 |
| 881662 | 25.0 | Female | 8.0 | 13.0 | 1.0 | 20.0 | Premium | Annual | 745.38 | 2.0 | 0.0 |
| 881663 | 26.0 | Male | 35.0 | 27.0 | 1.0 | 5.0 | Standard | Quarterly | 977.31 | 9.0 | 0.0 |
| 881664 | 28.0 | Male | 55.0 | 14.0 | 2.0 | 0.0 | Standard | Quarterly | 602.55 | 2.0 | 0.0 |
| 881665 | 31.0 | Male | 48.0 | 20.0 | 1.0 | 14.0 | Premium | Quarterly | 567.77 | 21.0 | 0.0 |
707454 rows × 11 columns
# Comparando os cancelamentos
# Antes (56% Cancelados ---> Com Monthly incluso)
display( tabela["cancelou"].value_counts(normalize=True) )
# Depois (46% Cancelados ---> Sem Monthly incluso)
display( tabela_filtrada["cancelou"].value_counts(normalize=True) )
cancelou 1.0 0.567105 0.0 0.432895 Name: proportion, dtype: float64
cancelou 0.0 0.539492 1.0 0.460508 Name: proportion, dtype: float64
import plotly.io as pio
# If this returns 'plotly_mimetype+notebook', comment the line below and run this block again
pio.renderers.default = 'plotly_mimetype'
# pio.renderers.default = 'notebook' # Uncomment this to allow HTML export
pio.renderers.default # Prints the default renderer
# ETAPA 4: Visualizando a causa dos cancelamentos com a ajuda de gráficos
# ------------- Rode esse bloco de código para gerar os gráficos ------------- #
# Criar gráficos para fazer a análise com o a ferramenta express da biblioteca plotly
import plotly.express as px
# Ver os diferentes tipos de gráficos oferecidos pelo plotly: https://plotly.com/python/
# O gráfico que será usado aqui é o histograma
# Criando um sample de DataFrame p/ passar as cores
data = pd.DataFrame({
'Category': ['A', 'B', 'C', 'A', 'B', 'C'],
'Value': [1, 2, 3, 4, 5, 6]
})
# Definindo um color map customizável
color_map = {
'A': 'red',
'B': 'green',
'C': 'blue'
}
for coluna in tabela.columns:
# Criar o gráfico
grafico = px.histogram(tabela_filtrada, x=coluna, color="cancelou", color_discrete_map=color_map, text_auto=True)
# Exibir o gráfico
grafico.show()
Após a análise do gráfico, percebeu-se 3 principais causas de cancelamento.¶
1- Assinatura mensal¶
Todos os clientes do contrato mensal cancelaram. Provavelmente porque o plano não oferece benefícios satisfatórios.
2- Quantidade de ligações feitas para o Call Center¶
Acima de 4 ligações feitas para o call center, o cliente tende a cancelar. Sendo que acima de 6 ligações, todos os clientes cancelaram.
3- Dias de atraso no pagamento da fatura¶
Acima de 20 dias de atraso, todos os clientes cancelaram.
# Novamente, é hora de eliminar os casos em que já se obteve conclusões acerca do cancelamento
condicao_callcenter = tabela_filtrada["ligacoes_callcenter"] <= 4
# Ficando agora com a tabela que só inclui aquelas resultados que não ligaram pro call center mais de 4 vezes
tabela_filtrada = tabela_filtrada[condicao_callcenter]
# Mesma coisa, só que com os dias de atraso agora
condicao_atraso = tabela_filtrada["dias_atraso"] <= 20
tabela_filtrada = tabela_filtrada[condicao_atraso]
display( tabela_filtrada["cancelou"].value_counts(normalize=True) )
cancelou 0.0 0.816037 1.0 0.183963 Name: proportion, dtype: float64
Conclusões da análise¶
De acordo com a simulação acima, retiradas as três principais causas, a média de cancelamentos cairia para apenas 26% (comparado com a taxa inicial de 53%).
Agora sim a taxa de cancelamentos está muito mais saudável e dentro do esperado.